<template>
  <Card class="full-height full-width topology-card">
    <template #title>
      业务请求调用链路：
      <Select
        v-model:value="selectedRequsetValue"
        style="width: 200px"
        :options="selectOptions"
        @change="changeRequest"
      ></Select>
      <Button
        type="primary"
        size="small"
        :href="selectedRequset.url"
        style="margin-left: 10px"
        target="_blank"
        >业务入口</Button
      >
    </template>

    <div
      id="d3-container"
      class="full-height full-width"
      style="flex-grow: 1; overflow: hidden"
    >
      <!-- D3 will append the SVG to this div -->
    </div>
  </Card>
</template>
  
  <script lang="ts">
import * as d3 from "d3";
import injectedNodeSvg from "../../../assets/topology-node/injectedNode.svg";
import injectingNodeSvg from "../../../assets/topology-node/injectingNode.svg";
import runningNodeSvg from "../../../assets/topology-node/runningNode.svg";
import { selectOptions, getData, remainingData } from "./dataJson.ts";
import { Card, Select, Button } from "ant-design-vue";
import { onBeforeMount, ref } from "vue";
import _ from "lodash";
import { InstanceItem } from "@/type/instances";
import eventBus from "../../../utils/eventBus";
import { loginApi } from "@/api/topology";
import { getChaosStatusApi } from "@/api/chaos";
import { useInstanceModuleStore } from "@/store/instance"; //
export default {
  name: "ForceDirectedGraph",
  components: {
    Select,
    Card,
    Button,
  },
  data() {
    const selectedRequsetValue = ref(selectOptions[0].value);
    const requestData = ref<InstanceItem[]>([]);
    const nodes = ref<any[]>([]);
    const links = ref<any[]>([]);
    const nodeWidth = 45;
    const nodeHeight = 45;
    const injectedChaos = ref<any>({});
    const selectedRequset = ref<any>(selectOptions[0]);

    const pollingInterval = ref<any>(null);

    requestData.value = getData(selectedRequsetValue.value);

    const transformData = (originalData) => {
      const nodesMap = new Map();
      const nodeLinks = [];

      // 将每个节点的状态添加到nodesMap中
      for (const node of originalData) {
        nodesMap.set(node.name, node.status);
      }

      // 遍历每个节点以生成links
      for (const node of originalData) {
        for (const nextNode of node.next) {
          if (nodesMap.has(nextNode)) {
            nodeLinks.push({
              source: node.name,
              target: nextNode,
              // 检查任何节点是否为error
              status:
                nodesMap.get(node.name) === "error" ||
                nodesMap.get(nextNode) === "error"
                  ? "error"
                  : "running",
            });
          }
        }
      }
      nodes.value = Array.from(nodesMap).map(([name, status]) => ({
        name,
        status,
      }));
      links.value = nodeLinks;
    };
    onBeforeMount(() => {
      transformData(requestData.value);
    });

    return {
      selectedRequsetValue,
      nodeWidth,
      nodeHeight,
      requestData,
      links,
      nodes,
      remainingData,
      getData,
      selectOptions,
      transformData,
      pollingInterval,
      injectedChaos,
      selectedRequset,
    };
  },
  mounted() {
    this.createForceDirectedGraph();
    this.startPolling();

    eventBus.on("start-polling", (data) => {
      this.startPolling();
    });
    eventBus.emit("change-request", this.selectOptions[0]);
    eventBus.on("restart", (data) => {
      this.startPolling();
      d3.select("#d3-container").select("svg").remove();
      this.createForceDirectedGraph();
    });
  },
  methods: {
    updateNodeAndLinks(nodeName: string, status: string) {
      d3.select(`#d3-container`)
        .selectAll(`[id*="${nodeName}"]`)
        .attr("xlink:href", this.getNodeBystatus(status));
      d3.select(`#d3-container`)
        .selectAll(`[id$='-${nodeName}']`)
        .attr("stroke", this.getColorBystatus(status))
        .select("path")
        .attr("fill", this.getColorBystatus(status));
    },
    getNodeBystatus(status: string) {
      switch (status) {
        case "injectFailed":
          return runningNodeSvg;
          break;
        case "running":
          return runningNodeSvg;
          break;
        case "injecting":
          return injectingNodeSvg;
          break;
        case "injected":
          return injectedNodeSvg;
          break;
      }
    },
    getColorBystatus(status: string) {
      switch (status) {
        case "running":
          return "#8bd496";
          break;
        case "injectFailed":
          return "#8bd496";
          break;
        case "injecting":
          return "#f7c01a";
          break;
        case "injected":
          return "#EF233C";
          break;
      }
    },
    createForceDirectedGraph() {
      const links = _.cloneDeep(this.links);
      const nodes = _.cloneDeep(this.nodes);

      const svg = d3
        .select("#d3-container")
        .append("svg")
        .attr("width", "100%")
        .attr("height", "100%");
      const svgWidth = svg.node().getBoundingClientRect().width;
      const svgHeight = svg.node().getBoundingClientRect().height;
      const marker = (color: string, markerId: string) => {
        svg
          .append("defs") //使用<defs>标签来定义可重复使用的对象。
          .append("marker") // 添加一个SVG的marker元素，表示箭头
          .attr("id", markerId)
          .attr("viewBox", "0 -5 10 10") // 设置marker的视图框，确定它的坐标和大小。
          .attr("refX", 6)
          .attr("refY", 0)
          // 确定箭头的大小。
          .attr("markerWidth", 4)
          .attr("markerHeight", 4)
          // 这使得箭头与线对齐，无论线的方向如何。
          .attr("orient", "auto")
          // 这里我们真正定义箭头的形状。它是一个简单的三角形。
          .append("path")
          .attr("d", "M0,-4L8,0L0,4L2,0")
          .attr("fill", color);

        return "url(#" + markerId + ")";
      };

      // 1. 确定根节点
      let allNextNames = new Set();
      this.requestData.forEach((node) => {
        node.next.forEach((name) => {
          allNextNames.add(name);
        });
      });

      // Find the first node that doesn't appear in any 'next' list
      let rootNode = this.requestData.find(
        (node) => !allNextNames.has(node.name)
      );
      const simulation = d3
        .forceSimulation(nodes)
        .force(
          "link",
          d3
            .forceLink(links)
            .id((d) => d.name)
            .distance(30)
        )
        .force("charge", d3.forceManyBody().strength(-80))
        // .force("center", d3.forceCenter(svgWidth / 2, svgHeight / 2))
        // // 对撞力
        .force("collide", d3.forceCollide(55)) // 调整为适当的值，例如节点的半径
        // .force("gravity", d3.forceY(d => d.depth * 100));
    //     .force("charge", d3.forceManyBody())
    // .force("x", d3.forceX(svgidth / 2))
    // .force("y", d3.forceY(d => d.depth * 100));
      const node = svg
        .append("g")
        .selectAll("image") //自定义图片
        .data(nodes)
        .enter()
        .append("image") //自定义图片
        .attr("xlink:href", (d) => {
          // 根据节点的某个属性决定图片的URL
          // 这里简单地为每个节点设置相同的图片
          return this.getNodeBystatus(d.status);
        })
        .attr("viewBox", "0 0 originalWidth originalHeight")
        .attr("preserveAspectRatio", "xMidYMid meet")
        //x 和 y 属性被设置为图片宽度和高度的一半的负值，以确保图片是围绕其中心定位的
        .attr("width", this.nodeWidth)
        .attr("height", this.nodeHeight)
        .attr("x", -this.nodeWidth / 2)
        .attr("y", -this.nodeHeight / 2)
        .attr("id", (d) => "node-" + d.name)
        .on("click", (event, d) => {
          this.handleNodeClick(d.name);
        })
        .on("mouseover", function (d) {
          d3.select(this) // 选择当前节点
            // .style("fill", highlightColor)
            .style("cursor", "pointer"); // 修改颜色以高亮节点
        });

      const vm = this;
      const link = svg
        .append("g")
        .selectAll("line")
        .data(links)
        .enter()
        .append("line")
        .attr("id", (d) => "link-" + d.source.name + "-" + d.target.name)
        .attr("stroke", (d) => {
          return this.getColorBystatus(d.status);
        })
        // .attr("stroke-opacity", 0.6)
        .attr("stroke-width", 2.5)
        .each(function (d: any) {
          const link = d3.select(this);
          const color = vm.getColorBystatus(d.status);
          const markerId = "marker-" + d.source.name + "-" + d.target.name;
          link.attr("marker-end", marker(color, markerId));
        });
      const text = svg
        .append("g")
        .selectAll("text")
        .data(nodes)
        .enter()
        .append("text")
        .text((d) => d.name)
        .attr("text-anchor", "middle") // 水平居中文本
        .attr("font-size", "12px") // 设置字体大小
        .attr("fill", "#777777"); // 设置文本颜色;

      const dots = svg
        .append("g")
        .selectAll("circle")
        .data(links)
        .enter()
        .append("circle")
        .attr("r", 4)
        .style("opacity", 0)
        .attr("fill", (d) => this.getColorBystatus(d.status)); // 设置与连接线相同的颜色

      const trange = () => {
        dots
          .attr("cx", (d) => d.x)
          .attr("cy", (d) => d.y)
          .transition()
          .style("opacity", 0.5)
          .duration(2000)
          .style("opacity", 1)
          .ease(d3.easePolyInOut)
          .attrTween("cx", (d) => {
            const interpolate = d3.interpolateNumber(
              this.getSourceXY(d)[0],
              this.getTargetXY(d)[0]
            );
            return function (t) {
              return interpolate(t);
            };
          })
          .attrTween("cy", (d) => {
            const interpolate = d3.interpolateNumber(
              this.getSourceXY(d)[1],
              this.getTargetXY(d)[1]
            );
            return function (t) {
              return interpolate(t);
            };
          })
          .transition() // 添加新的过渡
          .duration(2000)
          .style("opacity", 0) // 元素在动画结束时再次隐藏
          .on("end", function () {
            d3.select(this).attr("transform", "translate(0,0)");
            trange();
          });
      };
      console.log(nodes)

      simulation.on("tick", () => {
        link
          .attr("x1", (d) => this.getSourceXY(d)[0])
          .attr("y1", (d) => this.getSourceXY(d)[1])
          .attr("x2", (d) => this.getTargetXY(d)[0])
          .attr("y2", (d) => this.getTargetXY(d)[1]);

        text
          .attr("x", (d) => {
            return d.x;
          })
          .attr("y", (d) => d.y + this.nodeHeight / 1.5);
        node
          .attr("cx", (d) => d.x)
          .attr("cy", (d) => d.y)
          .attr("x", (d) => d.x - this.nodeWidth / 2)
          .attr("y", (d) => d.y - this.nodeHeight / 2);

        // trange();
        let minX = Infinity,
          minY = Infinity,
          maxX = -Infinity,
          maxY = -Infinity;
        node.each((d) => {
          minX = Math.min(minX, d.x - this.nodeWidth / 2);
          minY = Math.min(minY, d.y - this.nodeHeight / 2);
          maxX = Math.max(maxX, d.x + this.nodeWidth / 2);
          maxY = Math.max(maxY, d.y + this.nodeHeight / 2);
        });

        const contentWidth = maxX - minX + this.nodeWidth;
        const contentHeight = maxY - minY + this.nodeHeight;

        // 更新svg的viewBox
        svg.attr("viewBox", `${minX} ${minY} ${contentWidth} ${contentHeight}`);
        //     if (rootNode) {
        //   rootNode.y = 10;
        //   rootNode.fx = svgWidth / 2; // 如果你还想要它水平居中
        // }
      });
    },
    getSourceXY(d: any) {
      if (!d?.source || !d?.target) {
        return [0, 0]; // 返回默认坐标，或者其他适当的值
      }
      const deltaX = d.target.x - d.source.x;
      const deltaY = d.target.y - d.source.y;
      const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
      const normX = deltaX / dist;
      const normY = deltaY / dist;
      const sourceX = d.source.x + (this.nodeWidth / 2) * normX;
      const sourceY = d.source.y + (this.nodeHeight / 2) * normY;
      return [sourceX, sourceY];
    },
    getTargetXY(d: any) {
      if (!d?.source || !d?.target) {
        return [0, 0]; // 返回默认坐标，或者其他适当的值
      }
      const deltaX = d.target.x - d.source.x;
      const deltaY = d.target.y - d.source.y;
      const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
      const normX = deltaX / dist;
      const normY = deltaY / dist;
      const targetX = d.target.x - (this.nodeWidth / 2) * normX;
      const targetY = d.target.y - (this.nodeHeight / 2) * normY;
      return [targetX, targetY];
    },
    changeRequest(value, option) {
      this.selectedRequset = option;
      this.requestData = this.getData(this.selectedRequsetValue);
      this.transformData(this.requestData);
      d3.select("#d3-container").select("svg").remove();
      this.createForceDirectedGraph();
      // 更新折线图
      eventBus.emit("change-request", option);
      this.startPolling();
    },
    handleNodeClick(node: string) {
      eventBus.emit("click-instance", node);
    },
    startPolling() {
      if (this.pollingInterval) {
        clearInterval(this.pollingInterval);
      }
      this.pollingInterval = setInterval(this.pollingFunction, 500); //
    },
    async pollingFunction() {
      try {
        // 检查响应数据是否满足停止条件
        await this.fetchData();
        if (!this.injectedChaos || this.injectedChaos.status !== "injecting") {
          clearInterval(this.pollingInterval);
          this.pollingInterval = null;
        }
        if (this.injectedChaos?.params?.length > 0) {
          this.updateNodeAndLinks(
            this.injectedChaos.params[0].value,
            this.injectedChaos.status
          );
        }
      } catch (error) {
        console.error("Error during polling:", error);
        clearInterval(this.pollingInterval); // 在错误时你也可能想停止轮询
      }
    },
    async fetchData() {
      // 使用async/await处理异步请求
      try {
        const res = await getChaosStatusApi();
        if (res.code === "0") {
          this.injectedChaos = res.data.chaos;
          useInstanceModuleStore().setInstance(this.injectedChaos);
        }
      } catch (error) {
        throw error; // 重新抛出错误，这样pollingFunction中的catch块可以捕获它
      }
    },
  },
  beforeDestroy() {
    if (this.pollingInterval) {
      clearInterval(this.pollingInterval);
    }
  },
};
</script>
  
  <style lang="less" scoped>
.select-div {
  padding-left: 20px;
}
.topology-card :deep(.ant-card-body) {
  height: 100%;
}
</style>
  